package ifquery

import (
	"bytes"
	"context"
	"fmt"
	"sort"
	"strings"

	"gitee.com/bitwormhole/afsviewer/app/classes/fquery"
	"github.com/starter-go/afs"
	"github.com/starter-go/base/lang"
	"github.com/starter-go/mimetypes"
	"github.com/starter-go/vlog"
)

// FQueryService ...
type FQueryService struct {

	//starter:component

	_as func(fquery.Service) //starter:as("#")

	AFSService  afs.Service       //starter:inject("#")
	TypeManager mimetypes.Manager //starter:inject("#")

}

func (inst *FQueryService) _impl() fquery.Service {
	return inst
}

// Query ...
func (inst *FQueryService) Query(ctx context.Context, q *fquery.Query) (*fquery.Query, error) {
	req := &myFQueryRequest{
		context: ctx,
		parent:  inst,
		q1:      q,
		q2:      new(fquery.Query),
	}
	err := req.Fetch()
	if err != nil {
		return nil, err
	}
	return req.q2, nil
}

////////////////////////////////////////////////////////////////////////////////

type myFQueryRequest struct {
	parent  *FQueryService
	context context.Context
	q1      *fquery.Query
	q2      *fquery.Query
	uri     afs.URI
}

func (inst *myFQueryRequest) prepareLocation() (*fquery.Location, error) {

	q := inst.q1
	uri := q.URI
	if uri == "" {
		loc := &q.Location
		if loc.IsEmpty() {
			return nil, fmt.Errorf("location is empty")
		}
		return loc, nil
	}

	// use URI
	loc, err := fquery.ParseLocation(uri.String())
	if err != nil {
		return nil, err
	}
	if loc.IsAbs() {
		return loc, nil
	}

	// use ref
	ref, err := fquery.ParseLocation(q.Ref.String())
	if err != nil {
		return nil, err
	}
	if !ref.IsAbs() {
		return nil, fmt.Errorf("location is NOT abs")
	}

	return inst.addLocation(ref, loc)
}

func (inst *myFQueryRequest) addLocation(ref, to *fquery.Location) (*fquery.Location, error) {

	loc := &fquery.Location{}
	loc.Elements = append(loc.Elements, ref.Elements...)
	loc.Elements = append(loc.Elements, to.Elements...)

	for k, v := range ref.Query {
		loc.SetQueryParam(k, v)
	}
	for k, v := range to.Query {
		loc.SetQueryParam(k, v)
	}

	loc.HostIndex = ref.HostIndex
	loc.MaskLength = ref.MaskLength
	return loc, nil
}

func (inst *myFQueryRequest) rebuildLocation(dir afs.Path) (*fquery.Location, error) {
	uri := dir.GetURI()
	loc1 := &inst.q1.Location
	loc2, err := fquery.ParseLocation(uri.String())
	if err != nil {
		return nil, err
	}
	loc3 := &fquery.Location{}
	*loc3 = *loc2
	loc3.HostIndex = loc1.HostIndex
	loc3.MaskLength = loc1.MaskLength
	if loc3.HasSameMask(loc1) {
		return loc3, nil
	}
	return loc2, nil
}

func (inst *myFQueryRequest) Fetch() error {

	// prepare
	loc, err := inst.prepareLocation()
	if err != nil {
		return err
	}

	// fetch from AFS
	uri := loc.URI()
	ser := inst.parent.AFSService
	dir, err := ser.Fetch(uri)
	if err != nil {
		return err
	}

	loc, err = inst.rebuildLocation(dir)
	if err != nil {
		return err
	}

	// make result
	inst.q2.Self = inst.getSelfInfo(dir)
	inst.q2.Items = inst.listItemsInfo(dir)
	inst.q2.URI = loc.URI()
	inst.q2.Location = *loc
	inst.loadTypeInfo1()
	return nil
}

func (inst *myFQueryRequest) getSelfLocation(dir afs.Path) fquery.Location {
	uri := dir.GetURI()
	loc, err := fquery.ParseLocation(uri.String())
	if err != nil {
		loc = new(fquery.Location)
	}
	return *loc
}

func (inst *myFQueryRequest) getSelfInfo(dir afs.Path) *fquery.Item {
	uri := dir.GetURI()
	item := inst.makeItemInfo(dir)
	item.URI = uri.String()
	return item
}

func (inst *myFQueryRequest) listItemsInfo(dir afs.Path) []*fquery.Item {
	dst := make([]*fquery.Item, 0)
	isdir := dir.IsDirectory()
	if !isdir {
		return dst
	}
	namelist := dir.ListNames()
	sort.Strings(namelist)
	for _, name := range namelist {
		child := dir.GetChild(name)
		info := inst.makeItemInfo(child)
		dst = append(dst, info)
	}
	return dst
}

// func (inst *myFQueryRequest) findTypeInfo(name string, info afs.FileInfo) fquery.MIMEType {
// 	// suffix & type
// 	suffix := inst.getFileNameSuffix(name)
// 	ctype1 := fquery.TypeUnknown
// 	isdir := info.IsDirectory()
// 	if isdir {
// 		ctype1 = fquery.TypeFolder
// 	} else if info.IsFile() {
// 		opt := &mimetypes.Options{}
// 		ctype2, err := inst.parent.TypeManager.FindBySuffix(suffix, opt)
// 		if err == nil {
// 			ctype1 = ctype2.Type
// 		} else {
// 			ctype1 = fquery.TypeFile + fquery.MIMEType(";suffix="+suffix)
// 		}
// 	} else {
// 		ctype1 = fquery.TypeUnknown
// 	}
// 	return ctype1
// }

func (inst *myFQueryRequest) makeItemInfo(p afs.Path) *fquery.Item {

	name := p.GetName()
	info := p.GetInfo()
	t1 := info.CreatedAt()
	t2 := info.UpdatedAt()
	mode := info.Mode().String()

	// ctype := inst.findTypeInfo(name, info)

	// make item
	item := &fquery.Item{
		Name:      name,
		Size:      info.Length(),
		CreatedAt: lang.NewTime(t1),
		UpdatedAt: lang.NewTime(t2),
		Mode:      fquery.FileMode(mode),
		File:      info.IsFile(),
		Dir:       info.IsDirectory(),

		Link: false, // todo ...

		// ContentType: "",    //  ctype, 这个字段稍后再填充
	}
	return item
}

func (inst *myFQueryRequest) loadTypeInfo1() error {
	src := inst.q2.Items
	loader := &myFQueryTypeLoader{}
	loader.init(inst)
	for _, item := range src {
		err := loader.loadTypeForItem(item)
		if err != nil {
			vlog.Warn(err.Error())
		}
	}
	inst.q2.Types = loader.table2
	return nil
}

// func (inst *myFQueryRequest) loadTypeInfo2(key mimetypes.Type, info *fquery.TypeInfo) error {
// 	// suffix := info.Suffix
// 	// src, err := tm.FindBySuffix(suffix, nil)
// 	tm := inst.parent.TypeManager
// 	src, err := tm.Find(key, nil)
// 	if err != nil {
// 		return err
// 	}
// 	info.Info = *src
// 	return nil
// }

////////////////////////////////////////////////////////////////////////////////

type myFQueryTypeLoader struct {
	req    *myFQueryRequest
	table1 map[string]*fquery.TypeInfo         // map[suffix]info
	table2 map[mimetypes.Type]*fquery.TypeInfo // map[typename]info
}

func (inst *myFQueryTypeLoader) init(req *myFQueryRequest) {
	inst.req = req
	inst.table1 = make(map[string]*fquery.TypeInfo)
	inst.table2 = make(map[mimetypes.Type]*fquery.TypeInfo)
}

func (inst *myFQueryTypeLoader) loadTypeForItem(item *fquery.Item) error {

	if item.Dir {
		return inst.loadTypeForDir(item)
	}

	if item.File {
		return inst.loadTypeForFile(item)
	}

	return nil
}

func (inst *myFQueryTypeLoader) loadTypeForDir(item *fquery.Item) error {
	tn := fquery.TypeFolder
	info, err := inst.fetchTypeInfoByName(tn)
	if err != nil {
		return err
	}
	item.ContentType = info.Type
	return nil
}

func (inst *myFQueryTypeLoader) loadTypeForFile(item *fquery.Item) error {
	suffix := inst.getFileNameSuffix(item.Name)
	info, err := inst.fetchTypeInfoBySuffix(suffix)
	if err != nil {
		// try again as 'fs/file'
		info, err = inst.fetchTypeInfoByName(fquery.TypeFile)
		if err != nil {
			return err
		}
	}
	item.ContentType = info.Type
	return nil
}

func (inst *myFQueryTypeLoader) convertInfo(src *mimetypes.Info, suffix string) (*fquery.TypeInfo, error) {
	dst := &fquery.TypeInfo{}
	dst.Info = *src
	dst.Suffix = suffix
	return dst, nil
}

func (inst *myFQueryTypeLoader) putInfo(info1 *mimetypes.Info, suffix string) (*fquery.TypeInfo, error) {

	info2, err := inst.convertInfo(info1, suffix)
	if err != nil {
		return nil, err
	}
	inst.table1[suffix] = info2
	inst.table2[info1.Type] = info2
	return info2, nil
}

func (inst *myFQueryTypeLoader) fetchTypeInfoByName(name mimetypes.Type) (*fquery.TypeInfo, error) {

	// get in cache
	older := inst.table2[name]
	if older != nil {
		return older, nil
	}

	// load new
	tm := inst.req.parent.TypeManager
	info, err := tm.Find(name, nil)
	if err != nil {
		return nil, err
	}

	return inst.putInfo(info, "")
}

func (inst *myFQueryTypeLoader) fetchTypeInfoBySuffix(suffix string) (*fquery.TypeInfo, error) {

	// get in cache
	older := inst.table1[suffix]
	if older != nil {
		return older, nil
	}

	// load new
	tm := inst.req.parent.TypeManager
	info, err := tm.FindBySuffix(suffix, nil)
	if err != nil {
		return nil, err
	}

	return inst.putInfo(info, suffix)
}

func (inst *myFQueryTypeLoader) getFileNameSuffix(filename string) string {
	buffer := []byte(filename)
	i := bytes.LastIndexByte(buffer, '.')
	if i >= 0 {
		data := buffer[i:]
		str := string(data)
		return strings.ToLower(str)
	}
	return ""
}
